﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Tavis.Interfaces;

namespace Tavis {
    public class HypermediaContent : IHypermediaContent {
		public HypermediaState State { get; private set; }

		private static readonly Regex LinkHeaderParser = new Regex(@"<(?<href>[^>]*)>(?:\s*;\s*rel\s*=\s*(['""])(?<rel>.*?)\1)?", RegexOptions.CultureInvariant | RegexOptions.Compiled);

		private Action<IHypermediaContent> _ValueLoaded = delegate { };
		private Exception _ValueLoadException;

		private HttpContent _Content;
		private ILinkFactory _LinkFactory;
        private ISemanticsRepository _SemanticsRepository;
		
		private readonly Dictionary<Tuple<string, string>, List<ILink>> _LinkCache = new Dictionary<Tuple<string, string>, List<ILink>>();
		private readonly List<ILink> _HeaderLinks = new List<ILink>();

        private ILinkExtractor _LinkExtractor;
		private ILinkExtractor LinkExtractor {
			get { return _LinkExtractor ?? (_LinkExtractor = _SemanticsRepository.GetLinkExtractor(Value.GetType())); }
		}

		
		private object _Value;
		public object Value {
            get {
				WaitUntilFullyLoaded();

				if ((_ValueLoadException != null) && (State != HypermediaState.NoFormatter)) {
					throw _ValueLoadException;
				}

				return _Value;
			}
        }

		public Exception Exception {
			get { return _ValueLoadException; }
		}


		/// <remarks>It's potentially unsafe to access headers from multiple threads at once</remarks>
		public HttpContentHeaders Headers { get; private set; }
		

		public string MediaType {
			get {
				lock (Headers) {
					// Unsafe to access headers from multiple threads at once
					return Headers.ContentType.MediaType;
				}
			}
        }


		



		public HypermediaContent(ISemanticsRepository semanticsRepository, HttpContent content, ILinkFactory linkFactory, HttpResponseHeaders responseHeaders = null)
		{
			Init(semanticsRepository, content, linkFactory, responseHeaders);
        }

		internal HypermediaContent(ISemanticsRepository semanticsRepository, HttpContent content, ILinkFactory linkFactory, HttpResponseHeaders responseHeaders, Action<IHypermediaContent> onLoaded)
		{
			if (onLoaded != null) {
				_ValueLoaded = onLoaded;
			}

			Init(semanticsRepository, content, linkFactory, responseHeaders);
        }


		private void Init(ISemanticsRepository semanticsRepository, HttpContent content, ILinkFactory linkFactory, HttpResponseHeaders responseHeaders)
		{
			_SemanticsRepository = semanticsRepository;
			_LinkFactory = linkFactory;
			_Content = content;
			Headers = _Content.Headers;

			ExtractHeaderLinks(responseHeaders, _LinkFactory);

			BeginLoad();
		}

		
        public T GetLink<T>(string anchor = null) where T : ILink {
			if ((anchor == null) && (_HeaderLinks.OfType<T>().Any())) {
				return _HeaderLinks.OfType<T>().First();
			}

			var rel = _LinkFactory.GetRel(typeof(T));
			var key = new Tuple<string, string>(rel, anchor);
			if (!_LinkCache.ContainsKey(key)) {
				var link = LinkExtractor.GetLink(_LinkFactory, Value, rel, anchor);
				return link is T ? (T)link : default(T);
			}

			return _LinkCache[key].OfType<T>().FirstOrDefault();
        }


    	public ILink GetLink(string relation, string anchor = null) {
			var headerLinks = _HeaderLinks.Where(l => (l.Relation ?? "").ToLowerInvariant() == (relation ?? "").ToLowerInvariant());
			if ((anchor == null) && (headerLinks.Any())) {
				return headerLinks.First();
			}

			var key = new Tuple<string, string>(relation, anchor);
			if (!_LinkCache.ContainsKey(key)) {
				return LinkExtractor.GetLink(_LinkFactory, Value, relation, anchor);
			}

			return _LinkCache[key].FirstOrDefault();
        }


		public IEnumerable<T> GetLinks<T>(string anchor = null) where T : ILink
		{
			var rel = _LinkFactory.GetRel(typeof(T));
			var key = new Tuple<string, string>(rel, anchor);
			if (!_LinkCache.ContainsKey(key)) {
				_LinkCache[key] = LinkExtractor.GetLinks(_LinkFactory, Value, rel, anchor).ToList();
			}

			IEnumerable<ILink> links = _LinkCache[key];

			if (anchor == null) {
				links = links.Concat(_HeaderLinks);
			}

			return links.OfType<T>();
        }


        public IEnumerable<ILink> GetLinks(string relation = null, string anchor = null) {
			var key = new Tuple<string, string>(relation, null);
			if (!_LinkCache.ContainsKey(key)) {
				_LinkCache[key] = LinkExtractor.GetLinks(_LinkFactory, Value, relation, anchor).ToList();
			}

			IEnumerable<ILink> result = _LinkCache[key];

			if (anchor == null) {
				var headerLinks = _HeaderLinks.Where(l => relation == null || (l.Relation ?? "").ToLowerInvariant() == relation.ToLowerInvariant()).ToArray();
				if (headerLinks.Any()) {
					result = result.Concat(headerLinks);
				}
			}

			return result;
        }


		public IEnumerable<ILink> GetHeaderLinks()
		{
			return _HeaderLinks;
		}


		internal void SuppressValueLoadedEvent()
		{
			_ValueLoaded = delegate {  };
		}



		private void ExtractHeaderLinks(HttpResponseHeaders responseHeaders, ILinkFactory linkFactory)
		{
			foreach (var header in responseHeaders.Where(h => h.Key.ToLowerInvariant() == "link")) {
				var links = header.Value.First();

				foreach (Match match in LinkHeaderParser.Matches(links)) {
					string relation = null;
					if (match.Groups["rel"].Success) {
						relation = match.Groups["rel"].Value;
					}

					var link = linkFactory.CreateLink(relation);
					link.Target = new Uri(match.Groups["href"].Value, UriKind.RelativeOrAbsolute);

					_HeaderLinks.Add(link);
				}
			}
		}



		private MediaTypeFormatter GetFormatter(string mediaType)
		{
			var formatter = _SemanticsRepository.Formatters.FirstOrDefault(
				f => f.SupportedMediaTypes.Select(m => m.MediaType).Contains(mediaType)
			);

			if (formatter == null) {
				throw new FormatterNotFoundException(mediaType);
			}

			return formatter;
		}


		private void WaitUntilFullyLoaded()
		{
			while (State == HypermediaState.Loading) {
				Thread.Sleep(1);
			}
		}


		private void HandleError(Exception ex)
		{
			_Value = null;
			_ValueLoadException = ex;

			_Content.Dispose();
			_Content = null;

			State = HypermediaState.FormatterError;

			_ValueLoaded(this);
		}


		private void BeginLoad()
		{
			State = HypermediaState.Loading;

			MediaTypeFormatter formatter = null;
			try {
				formatter = GetFormatter(MediaType);
			}
			catch (FormatterNotFoundException formatterNotFoundException) {
				try {
					State = HypermediaState.NoFormatter;
					_ValueLoadException = formatterNotFoundException;

					var stream = new MemoryStream();
                    _Content.CopyToAsync(stream).RunSynchronously();
					stream.Position = 0;
					_Value = stream;

					_Content.Dispose();
					_Content = null;

					_ValueLoaded(this);
				}
				catch (Exception ex) {
					HandleError(ex);
				}
				return;
			}
			catch (Exception ex) {
				HandleError(ex);
				return;
			}
            var objectContent = new SimpleObjectContent<object>(_Content,formatter);
            var task = objectContent.ReadAsync();// formatter.ReadFromStreamAsync(typeof(object), _Content.ContentReadStream, _Content.Headers);
          
            
			task.ContinueWith(t => {
				if (t.Status == TaskStatus.Faulted) {
					HandleError(t.Exception);
					return;
				}

				_Value = t.Result;

				_Content.Dispose();
				_Content = null;

				State = HypermediaState.Loaded;

				_ValueLoaded(this);
			});
		}
    }



    public class SimpleObjectContent<T> : HttpContent
    {
        private readonly T _outboundInstance;
        private readonly HttpContent _inboundContent;
        private readonly MediaTypeFormatter _formatter;

        // Outbound constructor
        public SimpleObjectContent(T outboundInstance, MediaTypeFormatter formatter)
        {
            _outboundInstance = outboundInstance;
            _formatter = formatter;
        }

        //Inbound constructor
        public SimpleObjectContent(HttpContent inboundContent, MediaTypeFormatter formatter)
        {
            _inboundContent = inboundContent;
            _formatter = formatter;
        }

        protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
        {
            // FormatterContext is required by XmlMediaTypeFormatter, but is not used by WriteToStreamAsync of XmlMediaTypeFormatter!
            return _formatter.WriteToStreamAsync(typeof(T), _outboundInstance, stream, this.Headers, new FormatterContext(new MediaTypeHeaderValue("application/bogus"), false), null);
        }

        public Task<T> ReadAsync()
        {
            return this.ReadAsStreamAsync()
                .ContinueWith<object>(streamTask => _formatter.ReadFromStreamAsync(typeof(T), streamTask.Result, _inboundContent.Headers, new FormatterContext(new MediaTypeHeaderValue("application/bogus"), false)))
                .ContinueWith<T>(objectTask => (T)((Task<object>)(objectTask.Result)).Result);
        }


        protected override Task<Stream> CreateContentReadStreamAsync()
        {
            return _inboundContent.ReadAsStreamAsync();
        }

        protected override bool TryComputeLength(out long length)
        {
            length = -1L;
            return false;
        }
    }

}
